package la.renzhen.rtpt.spring.boot.conditional;

import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.MultiValueMap;

import java.lang.annotation.*;
import java.util.*;
import java.util.stream.StreamSupport;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ConditionalOnPrefixProperty.OnPrefixPropertyCondition.class)
public @interface ConditionalOnPrefixProperty {

    String[] value();

    boolean matchIfMissing() default false;

    @Order(Ordered.LOWEST_PRECEDENCE - 10)
    static class OnPrefixPropertyCondition extends SpringBootCondition {

        private List<AnnotationAttributes> annotationAttributesFromMultiValueMap(
                MultiValueMap<String, Object> multiValueMap) {
            List<Map<String, Object>> maps = new ArrayList<Map<String, Object>>();
            for (Map.Entry<String, List<Object>> entry : multiValueMap.entrySet()) {
                for (int i = 0; i < entry.getValue().size(); i++) {
                    Map<String, Object> map;
                    if (i < maps.size()) {
                        map = maps.get(i);
                    } else {
                        map = new HashMap<String, Object>();
                        maps.add(map);
                    }
                    map.put(entry.getKey(), entry.getValue().get(i));
                }
            }
            List<AnnotationAttributes> annotationAttributes = new ArrayList<AnnotationAttributes>(maps.size());
            for (Map<String, Object> map : maps) {
                annotationAttributes.add(AnnotationAttributes.fromMap(map));
            }
            return annotationAttributes;
        }

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
                    metadata.getAllAnnotationAttributes(ConditionalOnPrefixProperty.class.getName()));

            List<ConditionMessage> noMatch = new ArrayList<ConditionMessage>();
            List<ConditionMessage> match = new ArrayList<ConditionMessage>();
            for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
                ConditionOutcome outcome = determineOutcome(annotationAttributes, context);
                (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
            }
            if (!noMatch.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
            }
            return ConditionOutcome.match(ConditionMessage.of(match));
        }

        private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes, ConditionContext context) {
            List<String> prefixies = Arrays.asList(annotationAttributes.getStringArray("value"));
            boolean matchIfMissing = annotationAttributes.getBoolean("matchIfMissing");

            ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnPrefixProperty.class.getSimpleName());
            MutablePropertySources propSrcs = ((AbstractEnvironment) context.getEnvironment()).getPropertySources();
            Optional<String> key = StreamSupport.stream(propSrcs.spliterator(), false)
                    .filter(ps -> ps instanceof EnumerablePropertySource)
                    .map(ps -> ((EnumerablePropertySource) ps).getPropertyNames())
                    .flatMap(Arrays::<String>stream)
                    .filter(s -> prefixies.stream().filter(prefix -> s.startsWith(prefix)).findAny().isPresent())
                    .findAny();

            if (key.isPresent() ^ (!matchIfMissing)) {
                return ConditionOutcome.noMatch(key.isPresent() ?
                        message.found("find prefix").items(key.get()) :
                        message.didNotFind("cant find any prefix").atAll());
            }
            return ConditionOutcome.match(key.isPresent() ?
                    message.found("find prefix").items(key.get()) :
                    message.didNotFind("cant find any prefix").atAll());
        }
    }
}